SD58. 다중 스레드 프로그램에서 getlogin() 사용 — getlogin_r로 치환
Properties1
tags
—
17 min read
SD58. 다중 스레드 프로그램에서 getlogin() 사용 — getlogin_r로 치환
원본 항목: Part 7. API 오용 — 5. 다중 스레드 프로그램에서 getlogin() 사용CWE-558repo 폴더: sd58_getloginsim/ (빌드·실행은 §3-1)
목표 시간: 1회차 12분 / 2회차 7분 / 3회차 5분
Part 7(API 오용)의 마지막이자 58개 전체의 마지막 드릴이다.
실제 getlogin()은 제어 터미널(tty)이 있어야 동작해 테스트 환경에서 실패하기 쉬우므로, 정적 버퍼를 반환한다는 계약 자체를 흉내낸 대역 함수로 “스레드 간 공유”가 실제로 무엇을 망가뜨리는지 재현한다.
이 드릴의 문서 구성 (SD01~SD58 공통)
① 요구사항서 — 어떤 코드가 뚫려 있고, 무엇을 만드나.
② pseudo code 템플릿 — Bad 와 Good 의 뼈대이고, 회차마다 가린다.
③ 테스트 코드 — 공격은 막히고 정상은 통과해야 끝이다.
④ 자주 하는 실수
⑤ 모범답안 — 맨 뒤에 접어 둔다.
변형 A/B/C 는 같은 치환 규칙을 다른 소재로 다시 치는 것이다.
0. 이 드릴로 체화할 것
- struct passwd *pwd = getpwnam(getlogin()); /* 반환 버퍼가 스레드 간 공유 */- if (isTrustedGroup(pwd->pw_gid)) return 1;+ char id[MAX];+ if (getlogin_r(id, MAX) != 0) return 0; /* 지역 버퍼 + 실패 처리 */+ pwd = getpwnam(id);+ return isTrustedGroup(pwd->pw_gid) ? 1 : 0;
멀티스레드 환경에서 “정적 버퍼를 반환하는 함수”는 전부 _r(reentrant) 버전으로 바꾼다.
권한 판정처럼 중요한 값은 반드시 호출자 소유의 지역 버퍼에 고정해야 한다.
_r 버전이 없는 경우: 스레드 safe 한 함수가 제공되지 않으면 lock(뮤텍스)으로 호출~복사 구간 전체를 직렬화해야 한다.
호출만 lock으로 감싸면 부족하고, 반환 값을 지역 버퍼에 복사한 뒤에 unlock 한다.
static pthread_mutex_t login_lock = PTHREAD_MUTEX_INITIALIZER;char id[MAX];pthread_mutex_lock(&login_lock);char *p = getlogin(); /* _r 버전 없음 → lock 안에서 호출 */if (p == NULL) { pthread_mutex_unlock(&login_lock); return 0; }strncpy(id, p, MAX - 1); id[MAX - 1] = '\0'; /* 지역 버퍼로 복사 후 */pthread_mutex_unlock(&login_lock); /* unlock (모든 경로에서) */
회차별 진행
회차
변형
보는 것
순서
1회차
A 두 사용자의 연속 로그인 조회
① 요구사항서 ② pseudo code 전체
1. Bad 를 그대로 쳐서 취약점을 관찰한다. 2. Good pseudo 를 한 줄씩 코드로 바꾼다. 3. ③ 테스트를 실행한다.
2회차
B 권한 판정 로직까지 연결
① 요구사항서 ② pseudo code 의 번호와 제목만 (Level 2)
1. Bad 를 치고 취약점을 확인한다. 2. 번호만 보고 Good 을 쓴다. 3. ③ 테스트를 실행한다.
3회차
Cstrtok/ctime 등 다른 정적 버퍼 함수로 확장
① 요구사항서만
1. ③ 테스트를 먼저 읽는다. 2. 백지에서 Bad 부터 Good 까지 재현한다.
1회차에 막히면 같은 변형을 한 번 더 친다.
3회차를 통과하면 A 를 백지로 다시 친다.
int getlogin_like_good_r(const char *name, char *buf, size_t buf_size){ /* 아무것도 안 본다. * §1 요구사항서와 §0 의 치환 규칙만 보고 친다. * * 막히면 아래만 찾아본다. * - `_r` 버전이 없으면 lock 으로 **호출~복사 구간 전체**를 직렬화 * - 모든 경로에서 unlock */}
막혔을 때 찾는 순서
컴파일 에러가 나면 에러 문구를 본다.
unknown type name / implicit declaration 이면 include 누락이다.
man 3 <함수> 를 연다.
SYNOPSIS 맨 위의 #include 를 확인한다.
로직이 막히면 §3 테스트의 기대값을 먼저 읽는다.
공격 입력표(§1)와 테스트가 답이다.
진짜 레이스(두 스레드가 정확히 같은 순간에 겹쳐 실행)는 타이밍에 의존해 테스트가 실행할 때마다 다른 결과를 낼 수 있다.
이 드릴의 핵심은 “정확히 어느 순간에 겹치는가”가 아니라 **“결과를 담는 그릇(정적 버퍼)이 스레드 사이에 공유되는가”**이므로, 스레드를 순서대로 실행해도(join 후 다음 생성) 같은 결함이 100% 결정론적으로 드러난다 — 오히려 실전에서는 이보다 더 나쁜, 진짜 동시 접근으로 인한 반쯤 섞인 문자열까지 나올 수 있다.
눈여겨볼 점: getlogin_like_bad는 반환형이 const char *(내부 버퍼를 가리키는 포인터)이고, getlogin_like_good_r은 반환형이 int(성공/실패만 알림, 실제 데이터는 인자로 받은 buf에 쓴다)다.
_r 계열 함수들의 이 시그니처 패턴(포인터 반환 → 상태코드 반환 + out 파라미터)을 기억해두면 다른 정적 버퍼 함수를 만날 때도 같은 방식으로 치환할 수 있다.